package aaa

import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Calendar
import java.util.Date
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.http.RequestEntity
import org.springframework.http.ResponseEntity
import org.springframework.web.client.HttpClientErrorException


internal object S {
    const val RED: String = "\u001B[31m"
    const val GREEN: String = "\u001B[32m"
    const val YELLOW: String = "\u001B[33m"
    const val BLUE: String = "\u001B[34m"
    const val MAGENTA: String = "\u001B[35m"
    const val CYAN: String = "\u001B[36m"
    const val NOR: String = "\u001B[0m"
}

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
internal abstract class TestBase {

    // 🦜 : above is the kotlin way, below is the java way...
     @Autowired
     lateinit var restTemplate: TestRestTemplate
     @Autowired
     lateinit var json: ObjectMapper
     @Autowired
     lateinit var m: M

    @LocalServerPort
    protected var port: Int = 0

    protected fun url(): String {
        return "http://localhost:$port"
    }

    @Throws(JsonProcessingException::class)
    fun getJson(path: String): JsonNode {
        val req: RequestEntity<Void> = RequestEntity.get(url() + path).build()
        // restTemplate ?: throw RuntimeException("restTemplate is null")
        val r: ResponseEntity<String> = restTemplate.exchange<String>(req, String::class.java)
        log.info("r: {}",  r)
        // json ?: throw RuntimeException("json is null")
        assertTrue(r.getStatusCode().is2xxSuccessful())
        return json.readTree(r.getBody())
    }

    @Throws(JsonProcessingException::class)
    fun assertPostOk(body: String, method: String) {
        val req: RequestEntity<String> = RequestEntity.post(url() + method)
            .header("Content-Type", "application/json")
            .body<String>(body)

        // restTemplate ?: throw RuntimeException("restTemplate is null")
        val r: ResponseEntity<String> = restTemplate.exchange<String>(req, String::class.java)
        log.info("result for {}: {}", method,  r)
        Assertions.assertTrue(r.getStatusCode().is2xxSuccessful())
        Assertions.assertEquals(OK, r.getBody())
    }

    @Throws(JsonProcessingException::class)
    fun assertPostOk(node: ArrayNode, method: String) {
        // json ?: throw RuntimeException("json is null")
        val json_s: String = json.writeValueAsString(node)
        assertPostOk(json_s, method)
    }

    @Throws(JsonProcessingException::class)
    fun assertPostOk(node: ObjectNode, method: String) {
        val json_s: String = json.writeValueAsString(node)
        assertPostOk(json_s, method)
    }

    companion object {
        val log: Logger = LoggerFactory.getLogger(TestBase::class.java)
        fun old2New(d: Date): LocalDateTime {
            return LocalDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault())
        }

        fun new2Old(l: LocalDateTime): Date {
            return Date.from(l.atZone(ZoneId.systemDefault()).toInstant())
        }

        const val OK: String = "{\"code\":0,\"msg\":\"Success\"}"
    }
}




// @Disabled
internal class TestOld2 : TestBase() {
    private val log: Logger = LoggerFactory.getLogger(TestOld2::class.java)
    @Test
    fun set() {
        m.changePswdRecordRepo.deleteAll()

        val fmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
        val tmr: LocalDateTime = LocalDateTime.now().plusDays(1)
        val node: ObjectNode = json.createObjectNode()

        // 1. set the root pswd to `aaa`, expiry time to `tmr`
        val s: String = tmr.format(fmt)
        node.put("pswd", "aaa")
        node.put("expiryTime", s)

        assertPostOk(node, "/rootPswd/set")


        // 2. check that the db is updated
        assertEquals(1, m.changePswdRecordRepo.count())
        val r: Dt.ChangePswdRecord = m.changePswdRecordRepo.findAll().get(0)

        assertEquals("aaa", r.pswd)
        val r_expiryTime: LocalDateTime = r.expiryTime?.let { old2New(it) } ?: fail("expiryTime is null")
        assertEquals(s, r_expiryTime.format(fmt))

        // the createTime should be approximately now
        // val r_createTime: LocalDateTime = old2New(r.createTime)
        val r_createTime: LocalDateTime = r.createTime?.let { old2New(it) } ?: fail("createTime is null")
        assertTrue(r_createTime.isBefore(LocalDateTime.now().plusSeconds(1)))
        assertTrue(r_createTime.isAfter(LocalDateTime.now().plusSeconds(-1)))
    }
} // class TestOld2



/**
 * @brief The dangerous tests
 *
 * 🦜 : This should be run only when mock agent is on, because it will instruct
 * the agent to do something dangerous such as killing processes.
 */
internal class TestWithAgent {

    // @Disabled
    @Nested
    internal inner class TestProcesses : TestBase() {
        @Test
        @Throws(JsonProcessingException::class)
        fun testGetFailure() {
            // 🦜 : there should be some failures.. let's just test that.
            var pageNode = getJson("/process/getFailures?pageNum=1&pageSize=2")
            log.info("🦜 : getFailures: {}",  pageNode)
            // pageNode = pageNode["data"]
            // val voList = pageNode["voList"] as ArrayNode
            // Assertions.assertTrue(voList.size() > 0)
        }

        /*
         🦜 : Initially there should be some ok-register, but no
         must-register.... so we can test the must-register
         */
        @Test
        @Throws(JsonProcessingException::class)
        fun testGetOkRegister() {
            /*
             🦜 : Initially, there are some ok-registers such as the one for `systemd`
             */
            var pageNode = getJson("/process/getRegisters?pageNum=1&pageSize=2&type=ok")
            pageNode = pageNode["data"]
            val voList = pageNode["voList"] as ArrayNode
            Assertions.assertEquals(2, voList.size())
        }


        @Test
        @Throws(JsonProcessingException::class)
        fun testGetMustRegister() {
            /*
              🦜 : Initially, there are no must-registers
            */
            var pageNode = getJson("/process/getRegisters?pageNum=1&pageSize=2&type=must")
            pageNode = pageNode["data"]
            var voList = pageNode["voList"] as ArrayNode
            Assertions.assertEquals(0, voList.size())

            // put a must-register
            /*

              public static class ProcessRegisterBase{
              String exeName;
              String hash;
              String type;            // can be "ok" | "must"
              String reason;
              }
             */
            val node: ObjectNode = json.createObjectNode()
            node.put("exeName", "aaa")
            node.put("hash", "unknown")
            node.put("type", "must")
            node.put("reason", "bbb")

            assertPostOk(node, "/process/register")

            // 3. now there should be one must-register
            pageNode = getJson("/process/getRegisters?pageNum=1&pageSize=2&type=must")
            log.info("🦜 : getRegisters after adding one: {}",  pageNode)
            voList = pageNode["data"]["voList"] as ArrayNode
            Assertions.assertEquals(1, voList.size())
        }

        @Test
        @Throws(JsonProcessingException::class)
        fun testUnregister() {
            // 0. remember how many ok-registers there are
            var pageNode = getJson("/process/getRegisters?pageNum=1&pageSize=100&type=ok")
            log.info("🦜 : getRegisters before deleting one: {}",  pageNode)
            pageNode = pageNode["data"]
            var voList = pageNode["voList"] as ArrayNode
            val n = voList.size()

            // 1. let's delete the first one by id
            val id = voList[0]["id"].asInt()
            val req: RequestEntity<Void> = RequestEntity.get(url() + "/process/unregister?id=" + id)
                .build()
            Assertions.assertTrue(
                restTemplate.exchange<String>(req, String::class.java).getStatusCode().is2xxSuccessful()
            )

            // 2. now there should be one less
            pageNode = getJson("/process/getRegisters?pageNum=1&pageSize=100&type=ok")
            log.info("🦜 : getRegisters after deleting one: {}",  pageNode)
            voList = pageNode["data"]["voList"] as ArrayNode
            Assertions.assertEquals(n - 1, voList.size())
        }
    }                           //class TestProcesses

    // @Disabled
    @Nested
    internal inner class TestSafeBox : TestBase() {
        @Test
        fun create() {
            m.safeboxRecordRepo.deleteAll()

            val node: ObjectNode = json.createObjectNode()
            node.put("user", "aaa")
            node.put("passwd", "bbb")

            assertPostOk(node, "/safebox/create")
            /*
              🦜 : This should store this row (pkey = user)
             */
            assertEquals(1, m.safeboxRecordRepo.count())
        }

        @Test
        fun ls() {
            /*
             🦜 : This test will only works if `--mockMore=false` is given at the commandline of the agent
             */
            m.safeboxRecordRepo.deleteAll()

            val node: ObjectNode = json.createObjectNode()
            node.put("user", "aaa")
            node.put("passwd", "bbb")
            assertPostOk(node, "/safebox/create")


            /*
              2. 🦜 : now manually sync from the agent, we should give user `aaa` the list

              Files: []*pb.SimpleFileInfo
                  FileName: "aaa.txt", Size: 1000,
                  LastModifyTime: "2000-01-01 12:00:00",
                  LastAccessTime: "2222-12-31 12:00:00",
                  Permissions: "666",

                  FileName: "bbb.txt", Size: 100,
                  LastModifyTime: "2000-01-02 12:00:00",
                  LastAccessTime: "2222-12-30 12:00:00",
                  Permissions: "666",

                  FileName: "ccc.txt", Size: 10,
                  LastModifyTime: "2000-01-03 12:00:00",
                  LastAccessTime: "2222-12-29 12:00:00",
                  Permissions: "666",

                  So, now that we can get the list of files and test the sorting
             */
            /*
              🦜 : Update in 2024-01-02 no need to manually sync anymore, lazy sync is on.
             */

            /*
              3. 🦜 : now we can test the sorting, let's say we get a page of 2, sorted by `size` (ascending)?

              The result should be `ccc`, `bbb`.
             */

            /*
              RequestEntity<Void> req = RequestEntity.get(url() + "/safebox/ls?user=aaa&pageNum=1&pageSize=2&sortBy=size&asc=true").build();
              ResponseEntity<String> r = restTemplate.exchange(req, String.class);
              log.info("r: {}",  r);
              assertTrue(r.getStatusCode().is2xxSuccessful());
              JsonNode pageNode = json.readTree(r.getBody());
             */
            // 🦜 : factored ^v
            var pageNode = getJson("/safebox/ls?user=aaa&pageNum=1&pageSize=2&sortBy=size&asc=true")
            pageNode = pageNode["data"]
            // assertEquals(3, pageNode.get("totalCount").asInt());
            Assertions.assertEquals(3, pageNode["totalCount"].asInt())
            val voList = pageNode["voList"] as ArrayNode
            Assertions.assertEquals(2, voList.size())
            Assertions.assertEquals("ccc.txt", voList[0]["fileName"].asText())
            Assertions.assertEquals("bbb.txt", voList[1]["fileName"].asText())
        }

        val log: Logger = LoggerFactory.getLogger(TestSafeBox::class.java)
    }                           //class TestSafeBox


    // @Disabled
    @Nested
    internal inner class TestBasicCmdHistory : TestBase() {

        @Test
        fun getAll() {
                var pageNode = getJson("/cmdHistory/get?pageNum=1&pageSize=2")
                log.info("🦜 getCmdHistoryAll: {}", pageNode)
                pageNode = pageNode["data"]
                // assertEquals(20, pageNode.get("totalCount").asInt());
                val voList = pageNode["voList"] as ArrayNode
                Assertions.assertEquals(2, voList.size())
        } // @Test

        @Test
        fun getCmdHistoryBefore() {
            // get all that before 2005
            var pageNode = getJson("/cmdHistory/get?pageNum=1&pageSize=2&timeBefore=2024-01-01 01:01:01")
            pageNode = pageNode["data"]
            val voList = pageNode["voList"] as ArrayNode
            if (pageNode["totalCount"].asInt() == 0) {
                return
            }
            // the first one should be before 2024
            val tm = LocalDateTime.parse(voList[0]["time"].asText(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
            assertTrue(tm.isBefore(LocalDateTime.of(2024, 1, 1, 1, 1, 1)))
        }

        @Test
        fun exceptionHandler(){
            var j = getJson("/cmdHistory/throw")
            log.info("🦜 : exceptionHandler: {}",j)
            assertEquals(-1, j.get("code").asInt());
        }

        @Test
        fun getCmdHistoryById() {
            // 1. get a (for a valid id)
            var pageNode = getJson("/cmdHistory/get?pageNum=1&pageSize=2")
            pageNode = pageNode["data"]
            val voList = pageNode["voList"] as ArrayNode
            if (voList.size() == 0) {
                return
            }
            val id = voList[0]["id"].asInt()

            // 2. get that by id
            pageNode = getJson("/cmdHistory/get?pageNum=1&pageSize=2&id=$id")
            pageNode = pageNode["data"]
            assertEquals(1, pageNode["totalCount"].asInt())
        }
    } // TestBasicCmdHistory


    // @Disabled
    @Nested
    internal inner class TestNet : TestBase() {

        @Test
        fun firewallRules() {
            // 1. initialy there's no rules
            var pageNode = getJson("/net/getInboundRules")
            var voList = pageNode["data"] as ArrayNode
            assertEquals(0, voList.size())

            // 2. now set some rule
            val arr: ArrayNode = json.createArrayNode()

            arr.add(makeNetRule("tcp", "1234", "10.0.0.2/24", false))
            arr.add(makeNetRule("udp", "1234", "fd00::/64", true))
            arr.add(makeNetRule("icmp", "", "", false))
            assertPostOk(arr, "/net/setInboundRules")

            // 3. get the rules again. Now there should be 3.
            pageNode = getJson("/net/getInboundRules")
            voList = pageNode["data"] as ArrayNode
            assertEquals(3, voList.size())
        }

        private val log: Logger = LoggerFactory.getLogger(TestNet::class.java)

        fun makeNetRule(type: String, portRange: String, source: String, ipv6: Boolean): ObjectNode {
            val node: ObjectNode = ObjectMapper().createObjectNode()
            node.put("type", type)
            node.put("portRange", portRange)
            node.put("source", source)
            node.put("ipv6", ipv6)
            return node
        }

    }                       //class TestNet


    // @Disabled
    @Nested
    internal inner class TestBasic : TestBase() {

        @Test
        fun getManyCounters() {
            var j = getJson("/basic/getManyCounters")
            log.info("🦜 : getManyCounters: {}", j)
            j = j["data"]
            assertNotNull(j["x1"])
            // 🦜 : The Dt.ManyCounters
        }
        private val log: Logger = LoggerFactory.getLogger(TestBasic::class.java)


        @Test
        fun testGetUsers(){
            var j = getJson("/basic/getUsers")
            log.info("🦜 : getUsers: {}", j)
            j = j["data"]["voList"][0]
            assertNotNull(j["uid"])
        }

        @Test
        fun testApplicants(){
            // 1. add an Applicant
            val node: ObjectNode = json.createObjectNode()
            node.run {
                put("name", "aaa")
                put("phone", "phone")
                put("sfz", "sfz")
                put("code", "code")
                put("department", "department")
                put("expiryTime", "2024-01-01 01:01:01")
            }
            assertPostOk(node, "/basic/addApplicant")

            // 2. get the list
            var j = getJson("/basic/getApplicants?pageNum=1&pageSize=2&name=aaa")
            log.info("🦜 : getApplicants: {}", j)

            assertEquals(j["data"]["totalCount"].asInt(), 1)
            j = j["data"]["voList"][0]
            assertNotNull(j["id"])
            assertEquals("aaa", j["name"].asText());
        }

        @Test
        fun testRmApplicant(){

            // 1. get the list (🦜 : This should be the entire mocked list.)
            var j = getJson("/basic/getApplicants?pageNum=1&pageSize=20")
            // log.info("🦜 : getApplicants: {}", j)
            val c = j["data"]["totalCount"].asInt()
            log.info("🦜 : before removal got $c applicants")

            // 2. remove one
            val id = j["data"]["voList"][0]["id"].asInt()
            log.info("🦜 : removing applicant of id: {}", id)
            j = getJson("/basic/rmApplicant?id=$id")
            log.info("🦜 : rmApplicant: {}", j)
            assertEquals(0, j["code"].asInt())

            // 3. get the list again
            j = getJson("/basic/getApplicants?pageNum=1&pageSize=20")
            val c1 = j["data"]["totalCount"].asInt()
            log.info("🦜 : after removal: got $c1 applicants")
            assertEquals(c - 1, c1)
        }

        @Test
        fun getLoginHistory(){
            /*
             fun getLoginHistory(pageNum: Int, pageSize: Int, user: String?, sinceTime: String?, untilTime: String?, sortBy: String?, asc: Boolean?): W<Dt.Page<Dt.LoginInfo2?>>
             */
            var j = getJson("/basic/getLoginHistory?pageNum=1&pageSize=2")
            log.info("🦜 : getLoginHistory: {}", j)
        }

    } // class TestBasic

        /*

    @Disabled
    @Nested
    internal inner class TestRootPswd : TestBase() {
        private val log: Logger = LoggerFactory.getLogger(TestRootPswd::class.java)
    } // class TestRootPswd
         */


    // @Disabled
    internal class TestOld : TestBase() {
        @Test
        fun testGetRunDetail() {
            val r: String = restTemplate.getForObject<String>(url() + "/old/machine/getRunDetail", String::class.java)
            log.info("🦜 : getRunDetail : {}",r)
            var node: JsonNode = json.readTree(r)
            node = node["data"]
            // assertTrue(node["hourSinceBoot"].asInt() > 0)
            assertNotNull(node["hourSinceBoot"].asInt())
        }

        @Test
        fun testGetBashHistoryInfosByPages() {
            val r: String =
                restTemplate.getForObject<String>(
                    url() + "/old/bashHistory/getBashHistoryInfosByPages?pageNum=1&pageSize=3&userName=me",
                    String::class.java,
                )

            log.info("🦜 : getBashHistoryInfosByPages : {}",  r)
            var node: JsonNode = json.readTree(r)
            node = node["data"]
            val voList = node["voList"] as ArrayNode
            // assertEquals(1, voList.size());

            assertEquals("me", voList[0]["userName"].asText())
            assertEquals(false, voList[0]["isIllegal"].asBoolean())
        }

        @Test
        fun testLoginHistoryInfosByPages() // 🦜 : Update in 2024-01-11, use the new api
        {
            val r: String = restTemplate.getForObject<String>(
                url() + "/old/loginHistory/getLoginHistoryInfosByPages?pageNum=1&pageSize=3&userName=me",
                String::class.java
            )
            log.info("🦜 : getLoginHistoryInfosByPages : {}",  r)
            var node: JsonNode = json.readTree(r)
            node = node["data"]
            val voList = node["voList"] as ArrayNode
            // assertEquals(1, voList.size());
            assertTrue(voList.size() > 0)
            // assertEquals("me", voList.get(0).get("userName").asText());
        }

        @Test
        fun testGetMachineUsers() {
            val r: String = restTemplate.getForObject<String>(
                url() + "/old/machine/getMachineUsers?pageNum=1&pageSize=3",
                String::class.java
            )
            log.info("🦜 : getMachineUsers : {}",  r)
            var node: JsonNode = json.readTree(r)
            node = node["data"]
            val voList = node["voList"] as ArrayNode
            assertEquals(3, voList.size())
        }

    }

} // Test with Agent


